package service.server;

import iface.ForumServerServiceIface;

import java.security.SignatureException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Vector;

import common.AnonMath;
import common.ErrorCode;
import common.Message;
import common.SLA;
import common.ServerResponse;
import common.Settings;
import common.SignatureVerifier;
import common.SignedData;
import common.UserRPCClient;

public class ForumServer implements ForumServerServiceIface {

	private Integer[] acceptableArbitrators = new Integer[]{0,1,2,3,5,7,9,10,11};

	private Map<Integer,ForumClientSession> currentSessions;
	public  List<Message> msgBoard;
	private final String[] badWords = {"Happy", "Bunny", "Apocalypse", "Dan"};

	// services
	UserRPCClient arbitratorWevService = null;
	UserRPCClient CAWebService = null;

	// signatures
	public SignatureVerifier CASignatureVerifier = null;


	public ForumServer(){
		this.currentSessions = new HashMap<Integer,ForumClientSession>();
		this.msgBoard = new ArrayList<Message>();
		SetArbitratorService();
		setCAWebService();
		//setSignatureVerifier();
	}


	private void setSignatureVerifier() {
		Class[] returnTypes = new Class[]{byte[].class};
		String namespaceURI = Settings.CA_NS;
		String methodName = "getPublicKeyEncoding";
		Object[] mArgs = null;
		byte[] encodedKey = null;
		encodedKey = (byte[])this.CAWebService.wsCall(returnTypes, namespaceURI, methodName, mArgs)[0];
		CASignatureVerifier = new SignatureVerifier(encodedKey);
	}

	private void setCAWebService() {
		CAWebService = new UserRPCClient(Settings.CA_URI);
	}

	private void SetArbitratorService() {
		arbitratorWevService = new UserRPCClient(Settings.ARBITRATOR_URI);		
	}

	/**
	 * Initialize session with new user. Check signature and if agree on arbitrator
	 * @param userID - User temporary ID
	 * @param IDSignature - ID signature
	 * @param userAcceptableArbitrators - User acceptable array of arbitrators
	 * @return ServerResponse- if agree: true, agreed arbitrator ID
	 * 						   otherwise: false, array of server's acceptable arbitrators  
	 */
	public ServerResponse initSession(Integer userID, byte[] IDSignature, Integer[] userAcceptableArbitrators) {
		
		if (null==this.CASignatureVerifier)
			setSignatureVerifier();
		
		Vector<Integer> agreedArbitrators = new Vector<Integer>();
		ServerResponse sr = new ServerResponse();
		boolean success = false;
		int errorCode = ErrorCode.NO_AGREEMENT;
		Integer[] data = this.acceptableArbitrators.clone();
		if (!verifyUser(userID,IDSignature)){
			errorCode = ErrorCode.BAD_SIGNATURE;
			sr.setActionSucceeded(success);
			sr.setError(errorCode);
			sr.setIntData(data);
			return sr;
		}
		for (int i=0; i<this.acceptableArbitrators.length; i++){
			for (int j=0; j<userAcceptableArbitrators.length; j++){
				if (this.acceptableArbitrators[i].compareTo(userAcceptableArbitrators[j])==0){
					agreedArbitrators.add(this.acceptableArbitrators[i]);
				}
			}		
		}
		if (agreedArbitrators.size()>=Settings.sizeOfAgreementSet){
			success = true;
			data = agreedArbitrators.toArray(new Integer[0]);
			errorCode = ErrorCode.SUCCESS;
		}		
		sr.setActionSucceeded(success);
		sr.setError(errorCode);
		sr.setIntData(data);
		if (success){
			this.currentSessions.put(userID, new ForumClientSession(userID,agreedArbitrators.size()));
			currentSessions.get(userID).setArbitrators((Integer[]) agreedArbitrators.toArray(new Integer[0]));
			setUserPublicKeys(userID);
		}
		return sr;
	}

	private void setUserPublicKeys(Integer userID) {
		Integer[] userArbitrators = this.currentSessions.get(userID).getArbitrators();
		Integer arbitratorId;
		for (int i=0; i<userArbitrators.length; i++){
			arbitratorId = userArbitrators[i];
			this.currentSessions.get(userID).userPublicKeys[i] = getUserPublicKeyFromArbitrator(arbitratorId, userID);
		}
	}


	private byte[] getUserPublicKeyFromArbitrator(Integer arbitratorId,
			Integer userID) {
		byte[] ans;
		Class[] returnTypes = new Class[]{byte[].class};
		String namespaceURI = Settings.ARBITRATOR_NS;
		String methodName = "initServerSession";
		Object[] mArgs = new Object[]{arbitratorId,userID};
		ans = (byte[]) this.arbitratorWevService.wsCall(returnTypes, namespaceURI, methodName, mArgs)[0];
		if (null==ans)
			System.out.println("SERVER: got null from arbitrator while getting pub key for userID=" + userID);
		return ans;
	}


	private boolean verifyUser(Integer userID, byte[] iDSignature) {
		try {
			return CASignatureVerifier.verify(AnonMath.intToByteArray(userID), iDSignature);
		} catch (SignatureException e) {
			System.out.println("USER: "+e.getMessage());			
			e.printStackTrace();
			return false;
		}
	}


	/* (non-Javadoc)
	 * @see service.server.ForumServerServiceIface#postMessage(java.lang.Integer, java.lang.String, byte[])
	 */

	// rename client ID to userID
	public ServerResponse postMessage(Integer userID, byte[] IDSignature, String msg, SignedData[] signedMsg){
		boolean success = false;
		int errorCode = ErrorCode.BAD_SIGNATURE;
		String[] data = this.badWords.clone();
		
		ServerResponse sr = new ServerResponse();
		sr.setStrData(data);
		sr.setActionSucceeded(success);
		if (!verifyUser(userID,IDSignature)){
			sr.setError(ErrorCode.BAD_SIGNATURE);
			return sr;
		}
		ForumClientSession fcs = currentSessions.get(userID);
		if (!fcs.validate()){
			System.out.println("Server: trying to post for invalid session, clientid=" + userID +"");
			sr.setError(ErrorCode.SESSION_IMPROPERLY_INITIALIZED);
			sr.setIntData(new Integer[]{userID});
			return sr;
		}

		SignatureVerifier sv;
		byte[] publicKey;
		
		Integer[] userArbitrators = fcs.getArbitrators();
		//verify client message signatures
		sr.setError(errorCode);
		for (int i=0; i<userArbitrators.length; i++){
			publicKey = fcs.userPublicKeys[i];
			sv = new SignatureVerifier(publicKey);
			try {
				if (!sv.verify(msg.getBytes(), signedMsg[i].getSignature())){
					return sr;
				}
			} catch (SignatureException e) {
				System.out.println("SERVER: SignatureException: " + e.getMessage());
				e.printStackTrace();
				return sr;
			}
		}
		// Verify SLA		
		if (!checkClientMessage(msg)){
			System.out.println("Server: client id " + userID + " conatains illegal content");
			notifyArbitrators(userID,msg,signedMsg);
			sr.setError(ErrorCode.SLA_VIOLATION);
			sr.setStrData(this.badWords.clone());
			return sr;
		}
		else
			msgBoard.add(new Message(msg, userID));
		sr.setError(ErrorCode.SUCCESS);
		sr.setActionSucceeded(true);
		return sr;
	}

	private boolean notifyArbitrators(Integer userID, String msg,
			SignedData[] signedMsg) {
		boolean publishedShare = false;
		ForumClientSession fcs = currentSessions.get(userID);
		Integer[] userArbitrators = fcs.getArbitrators();
		for (int i=0; i<userArbitrators.length; i++){                    
			publishedShare = notifyArbitrator(userArbitrators[i],userID,msg,signedMsg[i].getSignature())|publishedShare;
		}
		return publishedShare;
	}

	private Boolean notifyArbitrator(Integer arbitratorID, Integer userID, String msg,
			byte[] signature) {
            //System.out.println(arbitratorID+", "+userID+" "+msg+" "+signature);
		Class[] returnTypes = new Class[]{Boolean.class};
		String namespaceURI = Settings.ARBITRATOR_NS;
		String methodName = "arbitrate";
		Object[] mArgs = new Object[]{arbitratorID,userID,msg,signature};
		return (Boolean) this.arbitratorWevService.wsCall(returnTypes, namespaceURI, methodName, mArgs)[0];
	}


	/* (non-Javadoc)
	 * @see service.server.ForumServerServiceIface#getMessages()
	 */
	public Message[] getMessages(){
		int sz;
		Message[] msgs;

		sz = this.msgBoard.size();
		msgs = new Message[sz];
		Iterator<Message> it = this.msgBoard.iterator();


		for (int i = 0; it.hasNext(); i++){
			msgs[i] = it.next();
		}
		return msgs;
	}

	/**
	 * Validate the user supplied message signature, before posting to msgBoard.
	 * @param msg - user message
	 * @return true - msg is ok, false  - msg contains forbidden words 
	 */
	private boolean checkClientMessage(String msg){
		return SLA.checkMessageSLA(msg);
	}
}
